/**
 * CS543 HW4 Joseph Meyer
 */
package edu.src.paging;

import java.util.ArrayList;
import java.util.List;

import edu.src.address.Address;
import edu.src.address.ReadWriteBit;
import edu.src.hardware.BackingStore;
import edu.src.hardware.PhysicalMemory;
import edu.src.logger.Logger;

/**
 * The Singleton Page Manager<p>
 * Implments <b>pure demand paging</b>
 * <p>
 * 
 * Handling Page Faults:<br>
 *  1. We check an internal table (usually kept with the process control block)
		for this process to determine whether the reference was a valid or an
		invalid memory access.<br>
	2. If the reference was invalid, we terminate the process. If it was valid but
		we have not yet brought in that page, we now page it in.<br>
	3. We find a free frame (by taking one from the free-frame list, for example).<br>
	4. We schedule a disk operation to read the desired page into the newly
		allocated frame.<br>
	5. When the disk read is complete, we modify the internal table kept with
		the process and the page table to indicate that the page is now in memory.<br>
	6. We restart the instruction that was interrupted by the trap. The process
		can now access the page as though it had always been in memory.<br>
	-pag 403<br>
 * <p>
 * Getting items from the page table:<br>
 * logical address gives us the index of the page table and offset into the frame.<br>
 *  - using the paging number, get the PageTableEntry from the page table.<br>
 *  - PageTableEntry will contain a frame and a valid/invalid bit.<br>
 *  - take the frame value and multiple by 256, to get us to the proper frame in the physical memory.<br>
 *  - once we are at the frame, the offset from the logical address tells us how far to go<br>
 *    into the frame to pull the correct item from memory. <br>
 *  - if the bit is invalid, the data is not in physical memory, should throw a page fault and <br>
 *    then should be loaded into physical memory.<br>
 *  (figure 8.12)<br>
 *  
 *  (2^8 = 256) <br>
 *  
 *  2^8 entries in the page table<br>
 *  page size is 2^8 bytes<br>
 *    256 byte pages <br>
 *  frame size is 2^8 bytes<br>
 *  total of 256 frames<br>
 *  
 *  16 entries in the TLB<br>
 *  <p>
 *  Physical Memory of 65,546 bytes (256 frames x 256-byte frame size) 
 *  The Physical Memory for this project is implmented by the <code>BackingStore</code>.
 *  <p>
 *  1000 entries means, technically there can be 1000 different pages requested. <br>
 *  But your memory has room only for 128 pages. But we know that we have 256 pages<br>
 *  available in .BIN file. Thus, some of the 1000 requests will be of some address<br>
 *  in the same pages. So, certainly there will be the cases where you need to<br>
 *  pull some of the currently residing pages from the memory and put new ones.<br>
 *   - Yusuf
 *  
 * @author meyer
 *
 */
public class PageTable {
	private Logger logger;
	private List<PageTableEntry> tableEntries;
	private static PageTable instance;
	private PhysicalMemory pMemory;
	public static final int TABLE_SIZE = 256;
	private int faultCount = 0;
	private int accessCount = 0;
	private int writtenToStore = 0;
	
	/**
	 * Singleton Constructor
	 * instantiates and loads the main table of entries
	 */
	private PageTable() {
		tableEntries = new ArrayList<>(TABLE_SIZE);
		logger = new Logger(this.getClass());
		pMemory = PhysicalMemory.getInstance();
		for (int i = 0; i < TABLE_SIZE; i++) {
			PageTableEntry entry = new PageTableEntry();
			tableEntries.add(i, entry);
		}
		
	}
	
	/**
	 * Marks all pages as unloaded.
	 * data from the backing store. 
	 */
	public void unload() {
		tableEntries.clear();
		pMemory.clearPhysicalMemory();
		faultCount = 0;
		accessCount = 0;
		writtenToStore = 0;
		for (int i = 0; i < TABLE_SIZE; i++) {
			PageTableEntry entry = new PageTableEntry();
			tableEntries.add(i, entry);
		}
	}
	
	/**
	 * Singleton accessor
	 * @return PageTable
	 */
	public static PageTable getInstance() {
		if (instance == null) {
			instance = new PageTable();
		}
		return instance;
	}

	/**
	 * Given a logical address, get the result from the paging table.
	 * @param address LogicalAddress
	 * @return PageResult
	 */
	public PageResult findEntry(Address address) {
		accessCount++;

		try {
			int pageNumber = address.getPageNumber(); 
			int offset = address.getOffset();
			
			PageTableEntry entry = tableEntries.get(pageNumber);
			Page p = entry.getPageAt(offset);
			
			if (p.getBit().equals(PageBit.INVALID)) {
				throw new PageTableFault();
			} else {
				//already loaded into physical memory
				int physicalMemoryAddress = p.getPhysicalMemoryAddress();
				try {
					byte value = pMemory.getByteFromPhysicalMemory(physicalMemoryAddress);
					return new PageResult(p.getFrameNumber(), offset, value, physicalMemoryAddress, address.getAddress());
				} catch (Exception e) {
					throw new PageTableFault();
				}
			}
			
		} catch (PageTableFault e) {
			//record a page fault occurred
			faultCount++;
			//load the byte into the paging table
			int pageNumber = address.getPageNumber(); 
			int offset = address.getOffset();
			
			//This is where the Swap space / Memory Replacement
			//occurs. It uses LRU algorithm to determine what to
			//replace if the requested memory slot is full.
			boolean proceed = pMemory.isFrameAvailable(offset);
			Page p = null;
			if (!proceed) {
				//The slot is full, swap something
				int victimPhysicalAddress = pMemory.findVictim();
				int pageSize = pMemory.getPageSize();
				int victimOffset = victimPhysicalAddress % (pageSize);
				int victimPageNo = (((victimPhysicalAddress - victimOffset) / (pageSize))) - 1;
				PageTableEntry entry = tableEntries.get(victimPageNo);
				p = entry.getPageAt(victimOffset);
				
				//check the 'dirty bit'
				if (p.getRwbit().equals(ReadWriteBit.WRITE)) {
					//we need to write this back to the backingstore before proceeding
					byte value = pMemory.getByteFromPhysicalMemory(p);
					BackingStore store = BackingStore.getInstance();
					store.writeByteTo(pageNumber, offset, value);
					writtenToStore++;
//					System.out.println("Encounted R/W bit at address: " + address.toString() + ". Writing value to disk before removing from Physical Memory.");
				}
				p.setBit(PageBit.INVALID);
			} else {
				//The slot is available, proceed.
				PageTableEntry entry = tableEntries.get(pageNumber);
				p = entry.getPageAt(offset);
			}
			
			p.loadIntoPage(address);
			//return the entry
			int physicalMemoryAddress = p.getPhysicalMemoryAddress();
			byte value = pMemory.getByteFromPhysicalMemory(physicalMemoryAddress);
			return new PageResult(p.getFrameNumber(), offset, value, physicalMemoryAddress, address.getAddress());
			
		} catch (Exception e) {
			//something else went wrong
			logger.error("Failed to load an entry from the PageTable", e);
		}
		return null;
	}

	/**
	 * @return the faultCount
	 */
	public int getFaultCount() {
		return faultCount;
	}

	/**
	 * @return the accessCount
	 */
	public int getAccessCount() {
		return accessCount;
	}
	
	/**
	 * @return double - the fault rate of the Page Table. a % from 0 to 100
	 */
	public double getFaultRate() {
		double result = ((double)faultCount) / ((double)accessCount);
		return result * 100;
	}
	
	/**
	 * @return double - the fault rate of the Page Table. a % from 0 to 100
	 */
	public double getSuccessRate() {
		int successCount = accessCount - faultCount;
		double result = ((double)successCount) / ((double)accessCount);
		return result * 100;
	}

	/**
	 * @return the writtenToStore
	 */
	public int getWrittenToStore() {
		return writtenToStore;
	}
	
	
	
}
